using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Diagnostics;
using System.IO;
using UmlDesigner.Gui;
using UmlDesigner.Model.WorkUnits;

namespace UmlDesigner.Model
{
    [Serializable]
    class LinkWidget : ItemWidget
    {
        internal LinkWidget(ItemWidget src, ItemWidget dst)
        {
            source = src;
            destination = dst;

            // Assign a default link type.
            if (source.IsNote() || destination.IsNote() || source.IsLink() || destination.IsLink())
            {
                linkType = LineType.NoteLink;
            }
            else
            {
                linkType = LineType.Association;
            }

            if (destination.IsClass())
            {
                instanceName = destination.GetClassName();
                if (Char.IsUpper(instanceName[0]))
                {
                    StringBuilder newName = new StringBuilder();
                    newName.Append(Char.ToLowerInvariant(instanceName[0]));
                    newName.Append(instanceName.Substring(1));
                    instanceName = newName.ToString();
                }
                else
                {
                    instanceName = "_" + instanceName;
                }
            }
        }

        /// <summary>
        /// An empty constructor which is only for use when copying the state 
        /// from somewhere else, etc.
        /// </summary>
        internal LinkWidget()
        {
            source = null;
            destination = null;
        }

        public string AccessType
        {
            get
            {
                return accessType;
            }
            set
            {
                Debug.Assert(value == "Public" || value == "Private" || value == "Internal");
                accessType = value;
            }
        }

        public string MultiplicityType
        {
            get
            {
                return multiplicityType;
            }
            set
            {
                Debug.Assert(value == "One To One" || value == "One To Many");
                multiplicityType = value;
            }
        }

        internal LineType LinkType
        {
            get
            {
                return linkType;
            }
            set
            {
                linkType = value;
            }
        }

        internal string Name
        {
            get
            {
                return instanceName;
            }
            set
            {
                instanceName = value;
            }
        }

        public string CollectionClass
        {
            get
            {
                return collectionClass;
            }
            set
            {
                Debug.Assert(value == "List" || value == "Dictionary");
                collectionClass = value;
            }
        }

        public bool GenerateProperty
        {
            get
            {
                return generateProperty;
            }
            set
            {
                generateProperty = value;
            }
        }

        internal ItemWidget GetSource()
        {
            return source;
        }

        internal ItemWidget GetDestination()
        {
            return destination;
        }

        internal override bool IsLink()
        {
            return true;
        }

        internal override bool IsPointWithin(Point p)
        {
            return GetBoundingRectangle().Contains(p);
        }

        //public override ItemWidget Clone()
        //{
        //    return new LinkWidget(source.Clone(), destination.Clone());
        //}

        internal override void PaintItem(Graphics e)
        {
            if (source == destination)
            {
                PaintBetweenClasses(e, source, destination, linkType);
            }
            else
            {
                if (!source.IsLink())
                {
                    PaintBetweenPoints(e, srcSlot.Position, destSlot.Position, linkType);
                }
                else
                {
                    PaintBetweenPoints(e, ((LinkWidget)source).GetMidPoint(), destSlot.Position, linkType);
                }
            }
        }

        internal void PaintBetweenPoints(Graphics e, Point[] points, LineType lineType)
        {
            Debug.Assert(points.Length >= 2);
            Color outlineColour = Color.FromArgb(95, 76, 126);

            GraphicsPath linePath = new GraphicsPath();
            for (int i = 0; i != points.Length - 1; i++)
            {
                linePath.AddLine(points[i], points[i + 1]);
            }

            if (lineType == LineType.Association)
            {
                GraphicsPath hPath = new GraphicsPath();

                // Create the outline for our custom end cap.
                const int arrowPartSize = 16;
                hPath.AddLine(new Point(0, 0), new Point(-arrowPartSize / 2, -arrowPartSize));
                hPath.AddLine(new Point(0, 0), new Point(arrowPartSize / 2, -arrowPartSize));

                //AdjustableArrowCap objCap = new AdjustableArrowCap(8, 8);
                CustomLineCap objCap = new CustomLineCap(null, hPath);

                // Set the start cap and end cap of the HookCap to be rounded.
                objCap.SetStrokeCaps(LineCap.Flat, LineCap.Flat);

                Pen outlinePen = new Pen(outlineColour);
                outlinePen.EndCap = LineCap.Custom;
                outlinePen.CustomEndCap = objCap;
                e.DrawPath(outlinePen, linePath);

                // Draw the link name
                if (instanceName != null)
                {
                    Font font = (Properties.Settings.Default.UseCustomFont) ? Properties.Settings.Default.CustomFont : SystemFonts.DefaultFont;

                    Matrix oldTransform = e.Transform;

                    e.TranslateTransform(points[0].X, points[0].Y);
                    double incline = CalcAngleOfInclination(points[0], points[1]);
                    e.RotateTransform((float)RadiansToDegrees(incline));
                    if (points[0].X < points[1].X)
                    {
                        e.TranslateTransform(linkLabelOffset, 0.0f);
                    }
                    else
                    {
                        e.TranslateTransform(-(linkLabelOffset + e.MeasureString(instanceName, font).Width), 0.0f);
                    }
                    e.DrawString(instanceName, font, Brushes.Black, new Point());
                    e.Transform = oldTransform;
                }

                // Draw the link multiplicity
                if (instanceName != null)
                {
                    Font font = (Properties.Settings.Default.UseCustomFont) ? Properties.Settings.Default.CustomFont : SystemFonts.DefaultFont;

                    string multiplicity;
                    if (IsOneToOneLink())
                    {
                        multiplicity = "1";
                    }
                    else
                    {
                        multiplicity = "*";
                    }

                    Matrix oldTransform = e.Transform;

                    e.TranslateTransform(points[1].X, points[1].Y);
                    double incline = CalcAngleOfInclination(points[0], points[1]);
                    e.RotateTransform((float)RadiansToDegrees(incline));
                    if (points[0].X < points[1].X)
                    {
                        e.TranslateTransform(-multplicityOffset, 0.0f);
                    }
                    else
                    {
                        e.TranslateTransform((multplicityOffset + e.MeasureString(multiplicity, font).Width), 0.0f);
                    }

                    e.DrawString(multiplicity, font, Brushes.Black, new Point());

                    e.Transform = oldTransform;
                }
            }
            else if (lineType == LineType.Inheritance)
            {
                GraphicsPath hPath = new GraphicsPath();

                // Create the outline for our custom end cap.
                const int arrowPartSize = 16;
                hPath.StartFigure();
                hPath.AddLine(new Point(0, 0), new Point(-arrowPartSize / 2, -arrowPartSize));
                hPath.AddLine(new Point(-arrowPartSize / 2, -arrowPartSize), new Point(arrowPartSize / 2, -arrowPartSize));
                hPath.CloseFigure();

                //AdjustableArrowCap objCap = new AdjustableArrowCap(8, 8);
                CustomLineCap objCap = new CustomLineCap(null, hPath);

                objCap.BaseInset = 16;

                // Set the start cap and end cap of the HookCap to be rounded.
                objCap.SetStrokeCaps(LineCap.Flat, LineCap.Flat);

                Pen outlinePen = new Pen(outlineColour);
                outlinePen.EndCap = LineCap.Custom;
                outlinePen.CustomEndCap = objCap;
                e.DrawPath(outlinePen, linePath);
            }
            else if (lineType == LineType.NoteLink)
            {
                Pen stippledPen = new Pen(outlineColour);
                float[] pattern = new float[2];
                pattern[0] = 5.0f;
                pattern[1] = 5.0f;
                stippledPen.DashPattern = pattern;
                e.DrawPath(stippledPen, linePath);
            }
        }

        private static double DistanceFromOrigin(Point point)
        {
            return Math.Sqrt((point.X * point.X) + (point.Y * point.Y));
        }

        /// <summary>
        /// Work out the angle of inclination (from the X axis) for two points.
        /// y / x = sin(a)
        /// </summary>
        /// <param name="p1"></param>
        /// <param name="p2"></param>
        /// <returns></returns>
        private static double CalcAngleOfInclination(Point p1, Point p2)
        {
            double dx = ((double)p1.X - (double)p2.X);
            double dy = ((double)p1.Y - (double)p2.Y);
            double result = 0.0;

            if (dx == 0.0)
            {
                dx = 1.0;
            }
            if (dy == 0.0)
            {
                dy = 1.0;
            }

            result = Math.Atan(dy / dx);

            Debug.Assert(result != double.NaN);
            return result;
        }

        private static double RadiansToDegrees(double result)
        {
            result = (result * 360.0 / (Math.PI * 2.0));
            return result;
        }

        internal void PaintBetweenPoints(Graphics e, Point srcPos, Point destPos, LineType lineType)
        {
            Point[] pts = new Point[2];
            pts[0] = srcPos;
            pts[1] = destPos;
            PaintBetweenPoints(e, pts, lineType);
        }

        private Rectangle GetBoundingRectangle()
        {
            Point srcPos, destPos;
            Point topLeft, bottomRight;

            if (source != destination)
            {
                srcPos = srcSlot.Position;
                destPos = destSlot.Position;

                topLeft = new Point(Math.Min(srcPos.X, destPos.X), Math.Min(srcPos.Y, destPos.Y));
                bottomRight = new Point(Math.Max(srcPos.X, destPos.X), Math.Max(srcPos.Y, destPos.Y));
            }
            else
            {
                topLeft = new Point(source.GetClassPosition().X + source.ClassRect.Width / 2, source.GetClassPosition().Y + source.ClassRect.Height / 2);
                bottomRight = new Point(source.GetClassPosition().X + source.ClassRect.Width + selfLinkLoopSize, source.GetClassPosition().Y + source.ClassRect.Height + selfLinkLoopSize);
            }

            // Expands out the rectangle.
            return new Rectangle(topLeft.X - 5, topLeft.Y - 5, (bottomRight.X - topLeft.X) + 10, (bottomRight.Y - topLeft.Y) + 10);
        }

        internal override void PaintSelectionOutline(Graphics e)
        {
            Pen stippledPen = new Pen(Color.Black);
            DrawSelection(e, stippledPen);
        }

        private void DrawSelection(Graphics e, Pen stippledPen)
        {
            Rectangle rect = GetBoundingRectangle();

            float[] pattern = new float[2];
            pattern[0] = 5.0f;
            pattern[1] = 5.0f;
            stippledPen.DashPattern = pattern;
            //stippledPen.DashStyle = DashStyle.Dash;
            e.DrawRectangle(stippledPen, rect);
        }

        public override void HandleFocusedOutline(Graphics g)
        {
            Pen stippledPen = new Pen(Color.BlueViolet, 2);
            DrawSelection(g, stippledPen);
        }

        public override void OnSetFocus()
        {
        }

        public override void OnKiilFocus()
        {
        }

        public static Point GetDestPoint(ItemWidget srcClass, ItemWidget destClass)
        {
            return destClass.GetDestPoint(srcClass);
        }

        internal override bool ItemWithinArea(Rectangle rect)
        {
            return rect.IntersectsWith(GetBoundingRectangle());
        }

        internal void AllocateSlotsForCircularRef()
        {
            Point ptRight = GetSource().Position;
            Point ptDown = GetSource().Position;

            ptRight.Offset(1000, 0);
            ptDown.Offset(0, 1000);

            srcSlot = GetSource().AssignSlot(this, ptRight);
            destSlot = GetDestination().AssignSlot(this, ptDown);
        }

        internal void PaintBetweenClasses(Graphics e, ItemWidget arrowSrc, ItemWidget arrowDest, LineType lineType)
        {
            if (arrowSrc == arrowDest)
            {
                // Its a link to itself, so deal with it specially...

                Point[] points = new Point[5];

                points[0] = new Point(srcSlot.Position.X, srcSlot.Position.Y);
                points[1] = new Point(srcSlot.Position.X + selfLinkLoopSize, srcSlot.Position.Y);
                points[2] = new Point(srcSlot.Position.X + selfLinkLoopSize, destSlot.Position.Y + selfLinkLoopSize);
                points[3] = new Point(destSlot.Position.X, destSlot.Position.Y + selfLinkLoopSize);
                points[4] = new Point(destSlot.Position.X, destSlot.Position.Y);

                PaintBetweenPoints(e, points, LineType.Association);

                return;
            }

            Point srcPos, destPos;
            srcPos = arrowSrc.GetSrcPoint(arrowDest.Position);
            destPos = GetDestPoint(arrowSrc, arrowDest);
            PaintBetweenPoints(e, srcPos, destPos, lineType);
        }

        public override Point GetSrcPoint(Point destPoint)
        {
            Trace.WriteLine("GetSrcPoint(): " + GetMidPoint().ToString());
            return GetMidPoint();
        }

        public override Point GetDestPoint(ItemWidget srcClass)
        {
            return GetMidPoint();
        }

        private Point GetMidPoint()
        {
            if (source != destination)
            {
                int midX = GetBoundingRectangle().X + (GetBoundingRectangle().Width / 2);
                int midY = GetBoundingRectangle().Y + (GetBoundingRectangle().Height / 2);
                return new Point(midX, midY);
            }
            else
            {
                int midX = source.GetClassPosition().X + source.ClassRect.Width + selfLinkLoopSize;
                int midY = source.GetClassPosition().Y + source.ClassRect.Height / 2 + selfLinkLoopSize / 2;
                return new Point(midX, midY);
            }
        }

        private string GetLinkTypeExpression()
        {
            string linkTypeString;
            if (IsOneToOneLink())
            {
                linkTypeString = destination.GetClassName();
            }
            else
            {
                linkTypeString = CollectionClass + "<" + destination.GetClassName() + ">";
            }
            return linkTypeString;
        }

        private bool IsOneToOneLink()
        {
            return MultiplicityType == "One To One";
        }

        public override void HandleDoubleClicked(UmlControl ctrl)
        {
            if (linkType != LineType.NoteLink)
            {
                LinkPropertiesForm form = new LinkPropertiesForm(this);
                form.ShowDialog();
            }
        }

        public override Slot AssignSlot(LinkWidget linkWidget, Point point)
        {
            return new Slot(GetMidPoint(), linkWidget);
        }

        internal override void Dump(TreeNode parent)
        {
            TreeNode linkParent = parent.Nodes.Add("Link: {" + GetHashCode() + "}");
            linkParent.Nodes.Add("Source: " + source.GetClassName() + " {" + source.GetHashCode() + "}");
            linkParent.Nodes.Add("Destination: " + destination.GetClassName() + " {" + destination.GetHashCode() + "}");
        }

        internal void AllocateSlots()
        {
            if (GetSource() != GetDestination())
            {
                srcSlot = GetSource().AssignSlot(this, GetDestination().Position);
                destSlot = GetDestination().AssignSlot(this, GetSource().Position);
            }
            else
            {
                AllocateSlotsForCircularRef();
            }
        }

        internal void DeallocateSlots()
        {
            GetSource().FreeSlot(this);
            GetDestination().FreeSlot(this);

            srcSlot = null;
            destSlot = null;
        }

        internal override void GenerateCode(CodeGenerationSettings settings)
        {
            Debug.WriteLine("Generating code for link: " + GetHashCode().ToString());

            if (destination.IsNote())
            {
                NoteWidget note = (NoteWidget)destination;
                note.GenerateCode(settings);
            }
            else if (destination.IsClass())
            {
                // Generate any notes attached to this link.
                foreach (LinkWidget link in links)
                {
                    if (link.GetDestination().IsNote())
                    {
                        link.GenerateCode(settings);
                    }
                }

                if (IsOneToOneLink())
                {
                    settings.WriteIndentedLine(AccessType.ToLower() + " " + GetLinkTypeExpression() + " " + instanceName + ";");
                }
                else
                {
                    settings.WriteIndentedLine(AccessType.ToLower() + " " + GetLinkTypeExpression() + " " + instanceName + " = new " + CollectionClass + "<" + destination.GetClassName() + ">();");
                }

                if (GenerateProperty && instanceName.Length > 1)
                {
                    string capitalizedInstanceName = Char.ToUpper(instanceName[0]) + instanceName.Substring(1);
                    if (capitalizedInstanceName == destination.GetClassName())
                    {
                        capitalizedInstanceName = capitalizedInstanceName + "Instance";
                    }

                    settings.WriteIndentedLine("");
                    settings.WriteIndentedLine("public " + GetLinkTypeExpression() + " " + capitalizedInstanceName);
                    settings.WriteIndentedLine("{");
                    settings.IncreaseIndent();
                    settings.WriteIndentedLine("get");
                    settings.WriteIndentedLine("{");
                    settings.WriteIndentedLine("    return " + instanceName + ";");
                    settings.WriteIndentedLine("}");
                    settings.WriteIndentedLine("set");
                    settings.WriteIndentedLine("{");
                    settings.WriteIndentedLine("    " + instanceName + " = value;");
                    settings.WriteIndentedLine("}");
                    settings.DecreaseIndent();
                    settings.WriteIndentedLine("}");
                    settings.WriteIndentedLine("");
                }
            }
        }

        internal void ShallowCopyLinkWidget(LinkWidget target)
        {
            target.srcSlot = null;
            target.destSlot = null;

            target.accessType = accessType;
            target.multiplicityType = multiplicityType;
            target.linkType = linkType;
            target.instanceName = instanceName;
            target.collectionClass = collectionClass;
            target.generateProperty = generateProperty;
            target.source = source;
            target.destination = destination;
            target.selfLinkLoopSize = selfLinkLoopSize;
            ShallowCopyItemWidget(target);

            target.AllocateSlots();
        }

        internal override Point GetClassPosition()
        {
            return GetBoundingRectangle().Location;
        }

        internal enum LineType { Association, Inheritance, NoteLink };
        const float linkLabelOffset = 20.0f;
        const float multplicityOffset = 30.0f;

        internal Slot srcSlot = null;
        internal Slot destSlot = null;

        string accessType = "Private";
        string multiplicityType = "One To One";
        protected LineType linkType = LineType.Association;
        protected string instanceName = null;
        private string collectionClass = "List";
        private bool generateProperty = false;
        protected ItemWidget source;
        protected ItemWidget destination;
        int selfLinkLoopSize = 50;
    }
}
